/*
 * $Id: LuaJavaAPI.java,v 1.4 2006/12/22 14:06:40 thiago Exp $
 * Copyright (C) 2003-2007 Kepler Project.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.keplerproject.luajava;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * Class that contains functions accessed by lua.
 * 
 * @author Thiago Ponte
 */
public final class LuaJavaAPI {

    private LuaJavaAPI() {
    }

    /**
     * Java implementation of the metamethod __index
     * 
     * @param luaState
     *            int that indicates the state used
     * @param obj
     *            Object to be indexed
     * @param methodName
     *            the name of the method
     * @return number of returned objects
     */
    public static int objectIndex(int luaState, Object obj, String methodName) throws LuaException {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            int top = L.getTop();

            Object[] objs = new Object[top - 1];

            Class<?> clazz;

            if (obj instanceof Class) {
                clazz = (Class<?>) obj;
            } else {
                clazz = obj.getClass();
            }

            Method[] methods = clazz.getMethods();
            Method method = null;

            // gets method and arguments
            for (int i = 0; i < methods.length; i++) {
                if (!methods[i].getName().equals(methodName))

                    continue;

                Class<?>[] parameters = methods[i].getParameterTypes();
                if (parameters.length != top - 1)
                    continue;

                boolean okMethod = true;

                for (int j = 0; j < parameters.length; j++) {
                    try {
                        objs[j] = compareTypes(L, parameters[j], j + 2);
                    } catch (Exception e) {
                        okMethod = false;
                        break;
                    }
                }

                if (okMethod) {
                    method = methods[i];
                    break;
                }

            }

            // If method is null means there isn't one receiving the given
            // arguments
            if (method == null) {
                throw new LuaException("Invalid method call. No such method.");
            }

            Object ret;
            try {
                if (Modifier.isPublic(method.getModifiers())) {
                    method.setAccessible(true);
                }

                if (obj instanceof Class) {
                    ret = method.invoke(null, objs);
                } else {
                    ret = method.invoke(obj, objs);
                }
            } catch (Exception e) {
                throw new LuaException(e);
            }

            // Void function returns null
            if (ret == null) {
                return 0;
            }

            // push result
            L.pushObjectValue(ret);

            return 1;
        }
    }

    /**
     * Java function to be called when a java Class metamethod __index is
     * called. This function returns 1 if there is a field with searchName and 2
     * if there is a method if the searchName
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param clazz
     *            class to be indexed
     * @param searchName
     *            name of the field or method to be accessed
     * @return number of returned objects
     * @throws LuaException
     */
    public static int classIndex(int luaState, Class<?> clazz, String searchName) throws LuaException {
        synchronized (LuaStateFactory.getExistingState(luaState)) {
            int res;

            res = checkField(luaState, clazz, searchName);

            if (res != 0) {
                return 1;
            }

            res = checkMethod(luaState, clazz, searchName);

            if (res != 0) {
                return 2;
            }

            return 0;
        }
    }

    /**
     * Pushes a new instance of a java Object of the type className
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param className
     *            name of the class
     * @return number of returned objects
     * @throws LuaException
     */
    public static int javaNewInstance(int luaState, String className) throws LuaException {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            Class<?> clazz;
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new LuaException(e);
            }
            Object ret = getObjInstance(L, clazz);

            L.pushJavaObject(ret);

            return 1;
        }
    }

    /**
     * javaNew returns a new instance of a given clazz
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param clazz
     *            class to be instanciated
     * @return number of returned objects
     * @throws LuaException
     */
    public static int javaNew(int luaState, Class<?> clazz) throws LuaException {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            Object ret = getObjInstance(L, clazz);

            L.pushJavaObject(ret);

            return 1;
        }
    }

    /**
     * Calls the static method <code>methodName</code> in class
     * <code>className</code> that receives a LuaState as first parameter.
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param className
     *            name of the class that has the open library method
     * @param methodName
     *            method to open library
     * @return number of returned objects
     * @throws LuaException
     */
    public static int javaLoadLib(int luaState, String className, String methodName) throws LuaException {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            Class<?> clazz;
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new LuaException(e);
            }

            try {
                Method mt = clazz.getMethod(methodName, new Class<?>[] { LuaState.class });
                Object obj = mt.invoke(null, new Object[] { L });

                if (obj != null && obj instanceof Integer) {
                    return ((Integer) obj).intValue();
                } else
                    return 0;
            } catch (Exception e) {
                throw new LuaException("Error on calling method. Library could not be loaded. " + e.getMessage());
            }
        }
    }

    private static Object getObjInstance(LuaState L, Class<?> clazz) throws LuaException {
        synchronized (L) {
            int top = L.getTop();

            Object[] objs = new Object[top - 1];

            Constructor<?>[] constructors = clazz.getConstructors();
            Constructor<?> constructor = null;

            // gets method and arguments
            for (int i = 0; i < constructors.length; i++) {
                Class<?>[] parameters = constructors[i].getParameterTypes();
                if (parameters.length != top - 1)
                    continue;

                boolean okConstruc = true;

                for (int j = 0; j < parameters.length; j++) {
                    try {
                        objs[j] = compareTypes(L, parameters[j], j + 2);
                    } catch (Exception e) {
                        okConstruc = false;
                        break;
                    }
                }

                if (okConstruc) {
                    constructor = constructors[i];
                    break;
                }

            }

            // If method is null means there isn't one receiving the given
            // arguments
            if (constructor == null) {
                throw new LuaException("Invalid method call. No such method.");
            }

            Object ret;
            try {
                ret = constructor.newInstance(objs);
            } catch (Exception e) {
                throw new LuaException(e);
            }

            if (ret == null) {
                throw new LuaException("Couldn't instantiate java Object");
            }

            return ret;
        }
    }

    /**
     * Checks if there is a field on the obj with the given name
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param obj
     *            object to be inspected
     * @param fieldName
     *            name of the field to be inpected
     * @return number of returned objects
     */
    @SuppressWarnings("unused")
    public static int checkField(int luaState, Object obj, String fieldName) throws LuaException {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            Field field = null;
            Class<?> objClass;

            if (obj instanceof Class) {
                objClass = (Class<?>) obj;
            } else {
                objClass = obj.getClass();
            }

            try {
                field = objClass.getField(fieldName);
            } catch (Exception e) {
                return 0;
            }

            if (field == null) {
                return 0;
            }

            Object ret = null;
            try {
                ret = field.get(obj);
            } catch (Exception e1) {
                return 0;
            }

            if (obj == null) {
                return 0;
            }

            L.pushObjectValue(ret);

            return 1;
        }
    }

    /**
     * Checks to see if there is a method with the given name.
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param obj
     *            object to be inspected
     * @param methodName
     *            name of the field to be inpected
     * @return number of returned objects
     */
    public static int checkMethod(int luaState, Object obj, String methodName) {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            Class<?> clazz;

            if (obj instanceof Class) {
                clazz = (Class<?>) obj;
            } else {
                clazz = obj.getClass();
            }

            Method[] methods = clazz.getMethods();

            for (int i = 0; i < methods.length; i++) {
                if (methods[i].getName().equals(methodName))
                    return 1;
            }

            return 0;
        }
    }

    /**
     * Function that creates an object proxy and pushes it into the stack
     * 
     * @param luaState
     *            int that represents the state to be used
     * @param implem
     *            interfaces implemented separated by comma (<code>,</code>)
     * @return number of returned objects
     * @throws LuaException
     */
    public static int createProxyObject(int luaState, String implem) throws LuaException {
        LuaState L = LuaStateFactory.getExistingState(luaState);

        synchronized (L) {
            try {
                if (!(L.isTable(2)))
                    throw new LuaException("Parameter is not a table. Can't create proxy.");

                LuaObject luaObj = L.getLuaObject(2);

                Object proxy = luaObj.createProxy(implem);
                L.pushJavaObject(proxy);
            } catch (Exception e) {
                throw new LuaException(e);
            }

            return 1;
        }
    }

    private static Object compareTypes(LuaState L, Class<?> parameter, int idx) throws LuaException {
        boolean okType = true;
        Object obj = null;

        if (L.isBoolean(idx)) {
            if (parameter.isPrimitive()) {
                if (parameter != Boolean.TYPE) {
                    okType = false;
                }
            } else if (!parameter.isAssignableFrom(Boolean.class)) {
                okType = false;
            }
            // replace for warning
            // obj = new Boolean(L.toBoolean(idx));
            obj = Boolean.valueOf(L.toBoolean(idx));
        } else if (L.type(idx) == LuaState.LUA_TSTRING.intValue()) {
            if (!parameter.isAssignableFrom(String.class)) {
                okType = false;
            } else {
                obj = L.toString(idx);
            }
        } else if (L.isFunction(idx)) {
            if (!parameter.isAssignableFrom(LuaObject.class)) {
                okType = false;
            } else {
                obj = L.getLuaObject(idx);
            }
        } else if (L.isTable(idx)) {
            if (!parameter.isAssignableFrom(LuaObject.class)) {
                okType = false;
            } else {
                obj = L.getLuaObject(idx);
            }
        } else if (L.type(idx) == LuaState.LUA_TNUMBER.intValue()) {
            // replace for warning
            // Double db = new Double(L.toNumber(idx));
            Double db = Double.valueOf(L.toNumber(idx));

            obj = LuaState.convertLuaNumber(db, parameter);
            if (obj == null) {
                okType = false;
            }
        } else if (L.isUserdata(idx)) {
            if (L.isObject(idx)) {
                Object userObj = L.getObjectFromUserdata(idx);
                if (!parameter.isAssignableFrom(userObj.getClass())) {
                    okType = false;
                } else {
                    obj = userObj;
                }
            } else {
                if (!parameter.isAssignableFrom(LuaObject.class)) {
                    okType = false;
                } else {
                    obj = L.getLuaObject(idx);
                }
            }
        } else if (L.isNil(idx)) {
            obj = null;
        } else {
            throw new LuaException("Invalid Parameters.");
        }

        if (!okType) {
            throw new LuaException("Invalid Parameter.");
        }

        return obj;
    }

}